# Souffle - A Datalog Compiler
# Copyright (c) 2021 The Souffle Developers. All rights reserved
# Licensed under the Universal Permissive License v 1.0 as shown at:
# - https://opensource.org/licenses/UPL
# - <souffle root>/licenses/SOUFFLE-UPL.txt

cmake_minimum_required(VERSION 3.15)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

include(CMakeDependentOption)

option(SOUFFLE_GIT "Enable/Disable git completion" ON)

if (SOUFFLE_GIT)
    find_package(Git REQUIRED)

    # GIT_TAGNAME identifies the current commit, it is closest tag in the
    # branch, possibly followed by number of additionnal commits and
    # abbreviated hash
    execute_process(COMMAND ${GIT_EXECUTABLE} describe --tags --always
                    WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
                    RESULT_VARIABLE GIT_RESULT
                    OUTPUT_VARIABLE GIT_TAGNAME)

    # GIT_CLOSEST_TAG is the closest tag in the branch
    execute_process(COMMAND ${GIT_EXECUTABLE} describe --tags --always --abbrev=0
                    WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
                    RESULT_VARIABLE GIT_RESULT
                    OUTPUT_VARIABLE GIT_CLOSEST_TAG)

    # Figure out the version number, depends on whether building from the git repo
    if (NOT GIT_RESULT EQUAL 0)
        # Not building from a git clone
        message(WARNING "Unable to find git repository: version number will be incomplete")
        set(GIT_PACKAGE_VERSION "UNKNOWN")
        set(PACKAGE_VERSION "")
    else()
        string(REGEX REPLACE "\n$" "" GIT_PACKAGE_VERSION "${GIT_TAGNAME}")
        message(STATUS "Building souffle version ${GIT_PACKAGE_VERSION}")

        string(REGEX REPLACE "\n$" "" GIT_CLOSEST_TAG "${GIT_CLOSEST_TAG}")
        # remove leading 'v' in tag if any
        string(REGEX REPLACE "^v" "" PACKAGE_VERSION "${GIT_CLOSEST_TAG}")
        # remove trailing '-something' in tag
        string(REGEX REPLACE "-.*$" "" PACKAGE_VERSION "${PACKAGE_VERSION}")

        # If building from a shallow clone where tag is not available.
        if (NOT ${PACKAGE_VERSION} MATCHES "^[0-9.]+$")
            message(WARNING "Cannot find a valid tag: cmake project version will be incomplete")
            set(PACKAGE_VERSION "")
        endif()

        message(STATUS "Package version ${PACKAGE_VERSION}")
    endif()
endif()

project(souffle VERSION "${PACKAGE_VERSION}"
                DESCRIPTION "A datalog compiler"
                LANGUAGES CXX)
include(CTest)

# Require out-of-source builds
file(TO_CMAKE_PATH "${PROJECT_BINARY_DIR}/CMakeLists.txt" LOC_PATH)
if(EXISTS "${LOC_PATH}")
    message(FATAL_ERROR "You cannot build in a source directory (or any directory with a CMakeLists.txt file). Please make a build subdirectory. Feel free to remove CMakeCache.txt and CMakeFiles.")
endif()


if (NOT CMAKE_BUILD_TYPE)
    set(CMAKE_BUILD_TYPE "Release" CACHE STRING
        "Choose the type of build" FORCE)
endif()
message(STATUS "Build Type: ${CMAKE_BUILD_TYPE}")

# Tells us whether we're building souffle as a main project
# or a subprobject
if (CMAKE_SOURCE_DIR STREQUAL PROJECT_SOURCE_DIR)
    set(SOUFFLE_MASTER_PROJECT On)
else()
    set(SOUFFLE_MASTER_PROJECT Off)
endif()

if (SOUFFLE_MASTER_PROJECT AND BUILD_TESTING)
    set(SOUFFLE_ENABLE_TESTING_DEFAULT ON)
else()
    set(SOUFFLE_ENABLE_TESTING_DEFAULT OFF)
endif()

# --------------------------------------------------
# User options available from the command line/cache
# --------------------------------------------------
option(SOUFFLE_DOMAIN_64BIT "Enable/Disable 64-bit number values in Datalog tuples" OFF)
option(SOUFFLE_USE_CURSES "Enable/Disable ncurses-based provenance display" ON)
option(SOUFFLE_SWIG "Enable/Disable all SWIG builds" OFF)
option(SOUFFLE_SWIG_PYTHON "Enable/Disable Python SWIG" OFF)
option(SOUFFLE_SWIG_JAVA "Enable/Disable Java SWIG" OFF)
option(SOUFFLE_USE_ZLIB "Enable/Disable use of libz file compression" ON)
option(SOUFFLE_USE_SQLITE "Enable/Disable use sqlite IO" ON)
option(SOUFFLE_USE_OPENMP "Enable/Disable use of openmp if available" ON)
option(SOUFFLE_SANITISE_MEMORY "Enable/Disable memory sanitiser" OFF)
option(SOUFFLE_SANITISE_THREAD "Enable/Disable thread sanitiser" OFF)
# SOUFFLE_NDEBUG = ON means -DNDEBUG on the compiler command line = no cassert
# Therefor SOUFFLE_NDEBUG = OFF means keep asserts
option(SOUFFLE_NDEBUG "Enable/Disable runtime checks even in release mode" OFF)
# By default, the "test/example" directory is not part of testing
# this flag enables the tests to run
option(SOUFFLE_TEST_EXAMPLES "Enable/Disable testing of code examples in tests/examples" OFF)
option(SOUFFLE_TEST_EVALUATION "Enable/Disable testing of evaluation examples in tests/examples" ON)
option(SOUFFLE_ENABLE_TESTING "Enable/Disable testing" ${SOUFFLE_ENABLE_TESTING_DEFAULT})
option(SOUFFLE_GENERATE_DOXYGEN "Generate Doxygen files (html;htmlhelp;man;rtf;xml;latex)" "")
option(SOUFFLE_CODE_COVERAGE "Enable coverage reporting" OFF)
option(SOUFFLE_BASH_COMPLETION "Enable/Disable bash completion" OFF)
option(SOUFFLE_USE_LIBFFI "Enable/Disable use of libffi" ON)
option(SOUFFLE_CUSTOM_GETOPTLONG "Enable/Disable custom getopt_long implementation" OFF)
option(SOUFFLE_TEST_DEBUG_REPORT "Enable/Disable generating debug-report for all tests" OFF)

if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
  set (USE_CLANG)
endif()

cmake_dependent_option(SOUFFLE_USE_LIBCPP "Link to libc++ instead of libstdc++" ON
  "USE_CLANG" OFF)

# Using Clang? Likely want to use `lld` too.
if (USE_CLANG)
    set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fuse-ld=lld")
    set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -fuse-ld=lld")
    set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -fuse-ld=lld")
endif()

# Add aditional modules to CMake
set(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake" ${CMAKE_MODULE_PATH})

if (SOUFFLE_USE_LIBCPP)
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -stdlib=libc++")
    set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -lc++abi")
    set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -lc++abi")
    set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -lc++abi")
endif()

if (WIN32)
  # Search libraries with and without 'lib' prefix.
  set(CMAKE_FIND_LIBRARY_PREFIXES ";lib")

  # Prefix all shared libraries with 'lib'.
  set(CMAKE_SHARED_LIBRARY_PREFIX "lib")

  # Prefix all static libraries with 'lib'.
  set(CMAKE_STATIC_LIBRARY_PREFIX "lib")

  SET( CMAKE_RUNTIME_OUTPUT_DIRECTORY_DEBUG "${OUTPUT_DIRECTORY}")
  SET( CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE "${OUTPUT_DIRECTORY}")
  SET( CMAKE_LIBRARY_OUTPUT_DIRECTORY_DEBUG "${OUTPUT_DIRECTORY}")
  SET( CMAKE_LIBRARY_OUTPUT_DIRECTORY_RELEASE "${OUTPUT_DIRECTORY}")
  SET( CMAKE_ARCHIVE_OUTPUT_DIRECTORY_DEBUG "${OUTPUT_DIRECTORY}")
  SET( CMAKE_ARCHIVE_OUTPUT_DIRECTORY_RELEASE "${OUTPUT_DIRECTORY}")
endif ()


if(EMSCRIPTEN)
  set(SOUFFLE_USE_SQLITE off)
  set(SOUFFLE_USE_OPENMP off)
  set(SOUFFLE_USE_ZLIB off)
  set(SOUFFLE_USE_LIBFFI off)
  set(SOUFFLE_USE_CURSES off)
  set(SOUFFLE_ENABLE_TESTING off)
endif ()
# --------------------------------------------------
# curses libraries for Provenance information
# --------------------------------------------------
if (SOUFFLE_USE_CURSES)
    find_package(Curses REQUIRED)
    if(NOT TARGET Curses::NCurses)
        add_library(Curses::NCurses UNKNOWN IMPORTED)
        set_target_properties(Curses::NCurses PROPERTIES
            IMPORTED_LOCATION "${CURSES_NCURSES_LIBRARY}"
            INTERFACE_INCLUDE_DIRECTORIES "${CURSES_INCLUDE_DIR}"
        )
    endif()
endif()

# --------------------------------------------------
# swig support
# --------------------------------------------------
if (SOUFFLE_SWIG)
    # Enable both
    set(SOUFFLE_SWIG_PYTHON "ON" CACHE STRING "" FORCE)
    set(SOUFFLE_SWIG_JAVA "ON" CACHE STRING "" FORCE)
endif()

if (SOUFFLE_SWIG_PYTHON OR SOUFFLE_SWIG_JAVA)
    find_package(SWIG REQUIRED)

    if (SOUFFLE_SWIG_PYTHON)
        find_package(Python3 3.7 REQUIRED)
    endif()

    if (SOUFFLE_SWIG_JAVA)
        find_package(Java REQUIRED
                     COMPONENTS Development)
        find_package(JNI REQUIRED)
        list(APPEND SOUFFLE_JAVA_INCLUDE_PATH ${JAVA_INCLUDE_PATH})
        list(APPEND SOUFFLE_JAVA_INCLUDE_PATH ${JAVA_INCLUDE_PATH2})
    endif()
endif()

# --------------------------------------------------
# flex and bison
# --------------------------------------------------
# See issue #2143.
if (CMAKE_HOST_SYSTEM_NAME MATCHES "Darwin")
    execute_process(
            COMMAND brew --prefix bison
            RESULT_VARIABLE BREW_BISON
            OUTPUT_VARIABLE BREW_BISON_PREFIX
            OUTPUT_STRIP_TRAILING_WHITESPACE
    )
    if (BREW_BISON EQUAL 0 AND EXISTS "${BREW_BISON_PREFIX}")
        message(STATUS "Found Bison keg installed by Homebrew at ${BREW_BISON_PREFIX}")
        set(BISON_EXECUTABLE "${BREW_BISON_PREFIX}/bin/bison")
    endif()

    execute_process(
            COMMAND brew --prefix flex
            RESULT_VARIABLE BREW_FLEX
            OUTPUT_VARIABLE BREW_FLEX_PREFIX
            OUTPUT_STRIP_TRAILING_WHITESPACE
    )
    if (BREW_FLEX EQUAL 0 AND EXISTS "${BREW_FLEX_PREFIX}")
        message(STATUS "Found Flex keg installed by Homebrew at ${BREW_FLEX_PREFIX}")
        set(FLEX_EXECUTABLE "${BREW_FLEX_PREFIX}/bin/flex")
    endif()
endif()
find_package(FLEX REQUIRED)
find_package(BISON "3.2" REQUIRED)

# --------------------------------------------------
# mcpp
# --------------------------------------------------
find_program(MCPP mcpp)

# --------------------------------------------------
# libz
# --------------------------------------------------
if (SOUFFLE_USE_ZLIB)
    find_package(ZLIB REQUIRED)
    if (SOUFFLE_ENABLE_TESTING)
        find_program(GZIP_BINARY gzip REQUIRED)
    endif()
endif()

# --------------------------------------------------
# sqlite
# --------------------------------------------------
if (SOUFFLE_USE_SQLITE)
    find_package(SQLite3 REQUIRED)
    if (SOUFFLE_ENABLE_TESTING)
        find_program(SQLITE3_BINARY sqlite3 REQUIRED)
    endif()
endif()

# --------------------------------------------------
# libffi
# --------------------------------------------------
if (SOUFFLE_USE_LIBFFI)
  find_package(libffi CONFIG QUIET)
  if (NOT libffi_FOUND)
    find_package(LibFFI REQUIRED)
  endif()
endif()

# --------------------------------------------------
# pthreads
# --------------------------------------------------
set(THREADS_PREFER_PTHREAD_FLAG ON)
find_package(Threads REQUIRED)

# --------------------------------------------------
# OpenMP
# --------------------------------------------------
if (SOUFFLE_USE_OPENMP)
  find_package(OpenMP)
endif()

# --------------------------------------------------
# Memory Sanitiser
# --------------------------------------------------
if (SOUFFLE_SANITISE_MEMORY)
  if (USE_CLANG)
    set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address,leak,undefined")
    set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address,leak,undefined")
  else()
    set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address,leak")
    set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address,leak")
  endif()
endif()

# --------------------------------------------------
# Thread Sanitiser
# --------------------------------------------------
if (SOUFFLE_SANITISE_THREAD)
    set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=thread")
    set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=thread")
endif()

# --------------------------------------------------
# Doxygen
# --------------------------------------------------
if (SOUFFLE_GENERATE_DOXYGEN)
    find_package(Doxygen REQUIRED dot)
    set(DOXYGEN_IN "${PROJECT_SOURCE_DIR}/doxygen.in")
    set(DOXYGEN_CFG "${PROJECT_SOURCE_DIR}/Doxyfile")
    set(DOXYGEN_DIR "${PROJECT_SOURCE_DIR}/doc")

    if ("htmlhelp" IN_LIST SOUFFLE_GENERATE_DOXYGEN)
        set(DOXYGEN_GENERATE_HTMLHELP "YES")
    endif()
    if ("man" IN_LIST SOUFFLE_GENERATE_DOXYGEN)
        set(DOXYGEN_GENERATE_MAN "YES")
    endif()
    if ("rtf" IN_LIST SOUFFLE_GENERATE_DOXYGEN)
        set(DOXYGEN_GENERATE_RTF "YES")
    endif()
    if ("xml" IN_LIST SOUFFLE_GENERATE_DOXYGEN)
        set(DOXYGEN_GENERATE_XML "YES")
    endif()
    if ("latex" IN_LIST SOUFFLE_GENERATE_DOXYGEN)
        set(DOXYGEN_GENERATE_LATEX "YES")
    endif()

    configure_file(${DOXYGEN_IN} ${DOXYGEN_CFG})

    add_custom_target(doxygen
        COMMAND ${DOXYGEN_EXECUTABLE} ${DOXYGEN_CFG}
        WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}
        COMMENT "Generating API documentation with Doxygen")
endif()

# --------------------------------------------------
# Generate the config file
# --------------------------------------------------
configure_file("${PROJECT_SOURCE_DIR}/cmake/config.h.in"
               "${PROJECT_BINARY_DIR}/src/config.h")

# --------------------------------------------------
# Change compile flags
# --------------------------------------------------
if (NOT SOUFFLE_NDEBUG)
    foreach(FLAG_VAR
            CMAKE_CXX_FLAGS_RELEASE
            CMAKE_CXX_FLAGS_MINSIZEREL
            CMAKE_CXX_FLAGS_RELWITHDEBINFO)

            # Remove/keep NDEBUG in Release builds
            string(REGEX REPLACE "-DNDEBUG" "" ${FLAG_VAR} "${${FLAG_VAR}}")
    endforeach()
endif()

# --------------------------------------------------
# Code Coverage Mode
# --------------------------------------------------

if(SOUFFLE_CODE_COVERAGE AND CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
    # Code Coverage Configuration
    add_library(coverage_config INTERFACE)
    # Add required flags (GCC & LLVM/Clang)
    target_compile_options(coverage_config INTERFACE
        -O0        # no optimization
        -g         # generate debug info
        --coverage # sets all required flags
    )
    if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.13)
        target_link_options(coverage_config INTERFACE --coverage)
    else()
        target_link_libraries(coverage_config INTERFACE --coverage)
    endif()
endif(SOUFFLE_CODE_COVERAGE AND CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")

# There are a few tests in src, so add it *after*
# including CTest
add_subdirectory(src)

if (SOUFFLE_ENABLE_TESTING)
    find_package(Python3 3.7 REQUIRED)
    add_subdirectory(src/tests)
    add_subdirectory(tests)
endif()


# --------------------------------------------------
# Installing bash completion file
# --------------------------------------------------
IF (SOUFFLE_BASH_COMPLETION) 
    if(NOT DEFINED BASH_COMPLETION_COMPLETIONSDIR)
        find_package (bash-completion)
        if (BASH_COMPLETION_FOUND)
            message(STATUS "Using bash completion dir ${BASH_COMPLETION_COMPLETIONSDIR}")
        else()
            set (BASH_COMPLETION_COMPLETIONSDIR "/etc/bash_completion.d")
            message (STATUS "Using fallback bash completion dir ${BASH_COMPLETION_COMPLETIONSDIR}")
        endif()
    else()
        message(STATUS "Using user-provided bash completion dir ${BASH_COMPLETION_COMPLETIONSDIR}")
    endif()

    install(
        FILES "${PROJECT_SOURCE_DIR}/debian/souffle.bash-completion"
        DESTINATION ${BASH_COMPLETION_COMPLETIONSDIR}
        RENAME "souffle"
    )
endif()


if (NOT WIN32)
# --------------------------------------------------
# CPack configuration
# --------------------------------------------------
execute_process(COMMAND bash "${PROJECT_SOURCE_DIR}/sh/check_os.sh"
                RESULT_VARIABLE CHECK_OS_RESULT
                OUTPUT_VARIABLE CHECK_OS_OUTPUT)

SET(CPACK_PACKAGE_CONTACT "Bernhard Scholz <bernhard.scholz@sydney.edu.au>")
SET(CPACK_PACKAGE_DESCRIPTION "Souffle - A Datalog Compiler")
SET(CPACK_PACKAGE_DESCRIPTION_SUMMARY "A Datalog Compiler")

# Use all available threads (primarily for compression of files)
SET(CPACK_THREADS 0)

# Make sure changelog, bash-completion and other important files in debian directory also packaged
SET(CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA "${PROJECT_SOURCE_DIR}/debian/changelog" "${PROJECT_SOURCE_DIR}/debian/souffle.bash-completion" "${PROJECT_SOURCE_DIR}/debian/copyright")

# --------------------------------------------------
# CPack configuration 
# --------------------------------------------------
if (CHECK_OS_RESULT EQUAL 0)
    if (CHECK_OS_OUTPUT MATCHES "DEBIAN")
        # Generate DEB packages
        SET(CPACK_GENERATOR "DEB")
        # --------------------------------------------------
        # Variables relevent to DEB packages
        # --------------------------------------------------

        # Specifying runtime dependencies
        set(CPACK_DEBIAN_PACKAGE_DEPENDS "g++ (>= 8), libffi-dev, libncurses5-dev, libsqlite3-dev, mcpp, zlib1g-dev, python3")

        # Auto-generate any runtime dependencies that are required
        SET(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)

        # Architectures are actually auto-detected so no need to set this variable
        # SET(CPACK_DEBIAN_PACKAGE_ARCHITECTURE "i386")
        INCLUDE(CPack)
    endif()

    if (CHECK_OS_OUTPUT MATCHES "FEDORA")
      SET(CPACK_RPM_PACKAGE_GROUP "Unspecified")
      SET(CPACK_RPM_PACKAGE_LICENSE "UPL-1.0 License")
      SET(CPACK_RPM_PACKAGE_VENDOR "Souffle-lang")
      SET(CPACK_RPM_PACKAGE_DESCRIPTION "${CPACK_PACKAGE_DESCRIPTION}")

      # Generate RPM packages
      SET(CPACK_GENERATOR "RPM")

      # --------------------------------------------------
      # Variables relevent to RPM packages
      # --------------------------------------------------

      # Specifying runtime dependencies
      if (MCPP)
        set(CPACK_RPM_PACKAGE_REQUIRES "gcc-c++ >= 8, libffi, libffi-devel, ncurses-devel, sqlite-devel, mcpp, zlib-devel, python3")
      else()
        # fedora versions >= 42 no longer have mcpp
        set(CPACK_RPM_PACKAGE_REQUIRES "gcc-c++ >= 8, libffi, libffi-devel, ncurses-devel, sqlite-devel, zlib-devel, python3")
      endif()

      # Note: By default automatic dependency detection is enabled by rpm generator.
      # SET(CPACK_RPM_PACKAGE_AUTOREQPROV "no")
      INCLUDE(CPack)
    endif()
endif()
endif()
